/*******************************************************************************
 * Package: com.song.flowable.service.impl
 * Type:    ModelServiceImpl
 * Date:    2023-11-27 21:06
 *
 * Copyright (c) 2023 LTD All Rights Reserved.
 *
 * You may not use this file except in compliance with the License.
 *******************************************************************************/
package com.song.flowable.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.song.common.util.CommUtil;
import com.song.common.util.JsonUtils;
import com.song.common.util.RedisServiceUtil;
import com.song.common.constants.CommonConstant;
import com.song.flowable.dto.FlowModelDTO;
import com.song.flowable.dto.ModelPageReqDTO;
import com.song.flowable.dto.ModelUpdateDTO;
import com.song.flowable.service.ModelFlowService;
import com.song.flowable.vo.ModelVO;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.editor.language.json.converter.BpmnJsonConverter;
import org.flowable.editor.language.json.converter.util.CollectionUtils;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.ui.common.service.exception.BadRequestException;
import org.flowable.ui.common.service.exception.BaseModelerRestException;
import org.flowable.ui.common.service.exception.InternalServerErrorException;
import org.flowable.ui.common.util.XmlUtil;
import org.flowable.ui.modeler.domain.Model;
import org.flowable.ui.modeler.model.ModelKeyRepresentation;
import org.flowable.ui.modeler.model.ModelRepresentation;
import org.flowable.ui.modeler.repository.ModelRepository;
import org.flowable.ui.modeler.repository.ModelSort;
import org.flowable.ui.modeler.service.FlowableModelQueryService;
import org.flowable.ui.modeler.serviceapi.ModelService;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * 功能描述： 流程模型
 * https://blog.csdn.net/weixin_45362084/article/details/113743474
 *
 * @author Songxianyang
 * @date 2023-11-27 21:06
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ModelFlowServiceImpl implements ModelFlowService {

    public static final String BPMN20_XML = "bpmn20.xml";

    private final RepositoryService repositoryService;


    private final ModelRepository modelRepository;

    private final ModelService modelService;

    protected final FlowableModelQueryService flowableModelQueryService;

    private final RedisServiceUtil redisServiceUtil;

    protected BpmnXMLConverter bpmnXmlConverter = new BpmnXMLConverter();
    protected BpmnJsonConverter bpmnJsonConverter = new BpmnJsonConverter();


    /**
     * 获得流程模型分页
     * todo 没有实现分页、过滤 后面可以通过传统的方式过滤JavaList
     *
     * @param dto 分页查询
     * @return 流程模型分页
     */
    @Override
    public List<ModelVO> getModelPage(ModelPageReqDTO dto) {
        List<Model> models = new ArrayList<>();
        StringBuilder filter = new StringBuilder();
        if (StringUtils.isNotEmpty(dto.getLikeName())) {
            filter.append(CommonConstant.NAME_ASC).append(StringUtils.trim(dto.getLikeName()).toLowerCase()).append(CommonConstant.NAME_ASC);
            models = modelRepository.findByModelTypeAndFilter(0, filter.toString(), ModelSort.MODIFIED_DESC);
        } else if (StringUtils.isNotEmpty(dto.getKey())) {
            models = modelRepository.findByKeyAndType(dto.getKey(), 0);
        } else {
            models = modelRepository.findByModelType(0, ModelSort.MODIFIED_DESC);
        }

        if (CollUtil.isEmpty(models)) {
            return Collections.EMPTY_LIST;
        }

        List<ModelVO> modelVOS = new ArrayList<>();

        models.forEach(v -> {
            ModelVO vo = getModelVO(v);
            modelVOS.add(vo);
        });
        return modelVOS;
    }


    /**
     * 删除模型
     *
     * @param id 模型id
     */
    @Override
    public Boolean deleteModel(String id) {
        modelService.deleteModel(id);
        return true;
    }


    /**
     * 添加流程模型
     *
     * @param req
     */

    public ModelRepresentation modelInsert(FlowModelDTO req) {
        ModelRepresentation model = new ModelRepresentation();
        model.setName(req.getName());
        model.setKey(req.getKey());
        model.setDescription(req.getDescription());
        model.setModelType(0);
        model.setKey(model.getKey().replaceAll(" ", ""));
        ModelKeyRepresentation modelKeyInfo = this.modelService.validateModelKey(new Model(), model.getModelType(), model.getKey());
        if (modelKeyInfo.isKeyAlreadyExists()) {
            throw new RuntimeException("key已存在: " + model.getKey() + "，key不可重复！");
        }
        String json = this.modelService.createModelJson(model);
        // todo 用户：admin
        Model newModel = modelService.createModel(model, json, "admin");
        return new ModelRepresentation(newModel);
    }


    /**
     * 修改模型部署的流程定义状态
     *
     * @param id    编号（1激活 2停止、挂起、暂停）
     * @param state 状态
     */
    @Override
    public String updateModelState(String id, Integer state) {
        // 校验流程model存在
        Model model = modelService.getModel(id);
        if (model == null) {
            throw new RuntimeException("流程模型不存在");
        }
        // 激活
        if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) {
            repositoryService.activateProcessDefinitionById(id, false, null);
            return "修改模型部署的流程定义状态成功";
        }
        // 停止
        if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) {
            // suspendProcessInstances = false，进行中的任务，不停止。
            // 原因：只要新的流程不允许发起即可，老流程继续可以执行。
            repositoryService.suspendProcessDefinitionById(id, false, null);
            return "修改模型部署的流程定义状态成功";
        }
        return "成功";
    }

    /**
     * 获得流程模型
     *
     * @param id 编号
     * @return ModelRespDTO
     */
    @Override
    public ModelVO getModel(String id) {

        ModelVO vo = new ModelVO();
        // 整合 redis 使用
        String value = redisServiceUtil.get("model:" + "getModel" + id);
        if (CommUtil.isEmpty(value)) {
            /**
             * 查询的key在redis中不存在，对应的id在数据库也不存在，此时被用户进行攻击，大量的请求会直接打在db上，造成宕机，影响整个系统，
             * 这种现象称之为缓存穿透。
             * 解决方案：把空的数据也缓存起来，比如空字符串，空对象，空数组或list
             */
            Model v = modelService.getModel(id);
            vo = getModelVO(v);
            if (CommUtil.isEmpty(v)) {
                // 设置过期时间5分钟
                redisServiceUtil.set("model:" + "getModel" + id, JsonUtils.toJson(vo), 5 * 60);
            } else {
                redisServiceUtil.set("model:" + "getModel" + id, JsonUtils.toJson(vo));
            }
        } else {
            // 取缓存里面得数据
            vo =  JSONUtil.toBean(value,ModelVO.class);
        }
        return vo;
    }

    private ModelVO getModelVO(Model v) {
        ModelVO vo = new ModelVO();
        vo.setId(v.getId());
        vo.setName(v.getName());
        vo.setKey(v.getKey());
        vo.setCreateName(v.getCreatedBy());
        vo.setLastUpdateTime(v.getLastUpdated());
        vo.setCreateTime(v.getCreated());
        vo.setLastUpdatedName(v.getLastUpdatedBy());
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery().processDefinitionKey(v.getKey()).list();
        vo.setPublishState(list.size() > 0 ? 1 : 0);
        vo.setFlowXml(getXml(v.getId()));
        // 模型修改json
        //vo.setModelEditorJson(v.getModelEditorJson());
        return vo;
    }

    /**
     * 获取模型
     *
     * @param id 模型id
     * @return
     */
    private String getXml(String id) {
        Model model = modelService.getModel(id);
        // 拼接 bpmn XML
        byte[] bpmnBytes = modelService.getBpmnXML(model);

        return StrUtil.utf8Str(bpmnBytes);
    }

    /**
     * 导出流程模型的XML
     *
     * @param id       模型id
     * @param response 响应
     */

    public void exportModelXML(String id, HttpServletResponse response) throws IOException {
        Model model = modelService.getModel(id);
        String name = model.getName().replaceAll(" ", "_") + ".bpmn20.xml";
        String encodedName = null;
        try {
            encodedName = "UTF-8''" + URLEncoder.encode(name, "UTF-8");
        } catch (Exception e) {
            log.warn("无法对名称进行编码:{}", name);
        }
        String contentDispositionValue = "attachment; filename=" + name;
        if (encodedName != null) {
            contentDispositionValue += "; filename*=" + encodedName;
        }
        response.setHeader("Content-Disposition", contentDispositionValue);
        if (model.getModelEditorJson() != null) {
            try {
                ServletOutputStream servletOutputStream = response.getOutputStream();
                response.setContentType("application/xml");
                BpmnModel bpmnModel = modelService.getBpmnModel(model);
                byte[] xmlBytes = modelService.getBpmnXML(bpmnModel);
                BufferedInputStream in = new BufferedInputStream(new ByteArrayInputStream(xmlBytes));
                byte[] buffer = new byte[8096];
                while (true) {
                    int count = in.read(buffer);
                    if (count == -1) {
                        break;
                    }
                    servletOutputStream.write(buffer, 0, count);
                }
                servletOutputStream.flush();
                servletOutputStream.close();
            } catch (BaseModelerRestException e) {
                throw e;
            } catch (Exception e) {
                log.error("无法生成 BPMN 2.0 XML", e);
                throw new InternalServerErrorException("无法生成 BPMN 2.0 xml");
            }
        }
    }

    /**
     * 修改流程模型
     *
     * @param dto
     */
    @SneakyThrows
    public Boolean updateModel(ModelUpdateDTO dto) {

        Model model = modelService.getModel(dto.getId());
        model.setName(dto.getName());
        model.setDescription(dto.getDescription());
        InputStream inputStream = IOUtils.toInputStream(dto.getFlowXml(), StandardCharsets.UTF_8);
        XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
        InputStreamReader xmlIn = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        XMLStreamReader xtr = xif.createXMLStreamReader(xmlIn);
        BpmnModel bpmnModel = bpmnXmlConverter.convertToBpmnModel(xtr);

        if (CollectionUtils.isEmpty(bpmnModel.getProcesses())) {
            throw new BadRequestException("No process found in definition ");
        }
        if (bpmnModel.getLocationMap().size() == 0) {
            BpmnAutoLayout bpmnLayout = new BpmnAutoLayout(bpmnModel);
            bpmnLayout.execute();
        }
        ObjectNode modelNode = bpmnJsonConverter.convertToJson(bpmnModel);
        modelService.saveModel(dto.getId(), dto.getName(), model.getKey(), dto.getDescription(), modelNode.toString(), true,
                "新版本注释！", "admin");
        return true;
    }

    /**
     * 导入模型
     *
     * @param file
     * @return
     */
    @Override
    public Boolean importModel(MultipartFile file) {
        flowableModelQueryService.importProcessModel(null, file);
        return true;
    }

    /**
     * 模型表的 act_de_model 里面字段值：model_key
     * 一定要和process id="model_key得值一样"
     *
     * select *
     *     from ACT_RE_PROCDEF
     *     where KEY_ = 'yudao_flow' and
     *           (TENANT_ID_ = ''  or TENANT_ID_ is null) and
     *           DERIVED_FROM_ is null and
     *           VERSION_ = (select max(VERSION_) from ACT_RE_PROCDEF where KEY_ = 'yudao_flow' and (TENANT_ID_ = '' or TENANT_ID_ is null))
     * 部署模型模型
     * @param id
     * @return
     */
    @Override
    public Boolean deploy(String id) {
        Model model = modelService.getModel(id);
        BpmnModel bpmnModel = modelService.getBpmnModel(model);
        //获取模型字节码
        byte[] bpmnXML = modelService.getBpmnXML(bpmnModel);
        repositoryService.createDeployment()
                .key(model.getKey()).name(model.getName())
                .addBytes(model.getName() + BPMN20_XML, bpmnXML)
                .deploy();
        return Boolean.TRUE;
    }
}
